﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2013-09-22 16:15
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading;
using BassDrop.Core.Common.General.Events;
using BassDrop.Core.Common.Utilities;

namespace BassDrop.Core.Common.General.Collections
{
	/// <summary>
	/// A collection that knows everything.<para/>
	/// <para/>
	/// It is<para/>
	///		- observable<para/>
	///		- optionally sorted<para/>
	///		- forwards property changes to any added items<para/>
	///			(if they implement INotifyPropertyChanged)<para/>
	///		- adds some extra methods for more comfortable usage<para/>
	///		- thread safe - standard events (PropertyChanged and CollectionChanged) will be fired 
	///			by the thread creating the collection (asynchronously).
	///			Other events are called directly.<para/>
	///	<para/>
	///	It might be a little slow though.
	/// </summary>
	/// <typeparam name="T">
	/// The type of elements in the collection.
	/// Using a type that implements INotifyPropertyChanged is recommended.
	/// </typeparam>
	/// <remarks>Partly from http://stackoverflow.com/a/5256827/301134 </remarks>
	public class SuperCollection<T> : ObservableCollection<T>
	{
		#region New events

		/// <summary>
		/// Forwards property change events from the items in the collection, even if event fires are disabled.
		/// Does not fire if an item is completely replaced by another one.
		/// </summary>
		public event EventHandler<CollectionItemChangedEventArgs> ItemChanged;

		/// <summary>
		/// Fired when an item is added to the collection, even if event fires are disabled.
		/// Does not get fired when an item is added to replace another one.
		/// </summary>
		public event EventHandler<CollectionItemAddedEventArgs> ItemAdded;

		/// <summary>
		/// Fired when an item is moved in the collection, even if event fires are disabled
		/// </summary>
		public event EventHandler<CollectionItemMovedEventArgs> ItemMoved;

		/// <summary>
		/// Fired when an item is removed from the collection, even if event fires are disabled.
		/// Does not fire when an item is replaced by another one or when all items are cleared.
		/// </summary>
		public event EventHandler<CollectionItemRemovedEventArgs> ItemRemoved;

		/// <summary>
		/// Fired when the items are cleared from the collection, even if event fires are disabled
		/// </summary>
		public event EventHandler<CollectionItemsClearedEventArgs> ItemsCleared;

		/// <summary>
		/// Fired when an item is replaced in the collection, even if event fires are disabled
		/// </summary>
		public event EventHandler<CollectionItemReplacedEventArgs> ItemReplaced;

		/// <summary>
		/// Fired when an item is added to or removed from the collection or the order of the items is changed.
		/// (The CollectionChanged event fires when the properties of the items change as well.)
		/// </summary>
		public event NotifyCollectionChangedEventHandler ItemCollectionChanged;

		#endregion New events

		#region New fields and properties

		/// <summary>
		/// The SynchronizationContext of the thread creating this object
		/// </summary>
		private SynchronizationContext m_creatorContext;

		/// <summary>
		/// Synchronization context of the thread owning this collection
		/// (the one that invoked the constructor)
		/// </summary>
		private SynchronizationContext m_ownerContext;

		/// <summary>
		/// The managed thread id of the thread creating this object
		/// </summary>
		private int m_creatorContextManagedThreadId;

		/// <summary>
		/// The managed thread id of the thread associated with m_ownerContext
		/// </summary>
		private int m_ownerContextManagedThreadId;

		/// <summary>
		/// Suppresses firing ItemCollectionChanged
		/// </summary>
		private bool m_suppressItemCollectionChanged;

		/// <summary>
		/// If true, the collection doesn't fire events
		/// </summary>
		public bool NotificationsSuppressed
		{
			get;
			protected set;
		}

		/// <summary>
		/// Is the collection sorted.
		/// 
		/// T needs to implement IComparable to allow setting this to true.
		/// </summary>
		private bool m_sorted;
		/// <summary>
		/// Is the collection sorted.
		/// 
		/// T needs to implement IComparable&lt;T&gt; to allow setting this to true.
		/// </summary>
		public bool Sorted
		{
			get
			{
				lock ( ((ICollection) this).SyncRoot )
				{
					return m_sorted;
				}
			}
			set
			{
				bool fireEvent = false;

				lock ( ((ICollection) this).SyncRoot )
				{
					if ( m_sorted != value )
					{
						m_sorted = value;

						if ( m_sorted )
						{
							if ( !(typeof( IComparable<T> ).IsAssignableFrom( typeof( T ) )) )
							{
								throw new InvalidOperationException(
									string.Format(
									"Automatic sorting cannot be enabled on a SuperCollection of {0}: it does not implement IComparable<{0}>.",
									typeof( T ).Name ) );
							}
							else
							{
								Sort();
							}
						}

						fireEvent = true;
					}
				}

				if ( fireEvent )
				{
					OnPropertyChanged( new PropertyChangedEventArgs( "Sorted" ) );
				}
			}
		}


		#endregion New fields and properties

		#region New constructors

		/// <summary>
		/// Constructs a new SuperCollection
		/// 
		/// Can only be used on a thread having a SynchronizationContext.
		/// Recommended to use on the main thread.
		/// </summary>
		public SuperCollection()
			: this( null, false )
		{
		}

		/// <summary>
		/// Constructs a new SuperCollection with the ability to make it sorted
		/// 
		/// Can only be used on a thread having a SynchronizationContext.
		/// Recommended to use on the main thread.
		/// </summary>
		/// <param name="sorted">
		/// Is the collection sorted.
		/// T needs to implement IComparable to allow setting this to true.
		/// </param>
		public SuperCollection( bool sorted )
			: this( null, sorted )
		{
		}

		/// <summary>
		/// Constructs a new SuperCollection with the ability to make it sorted and copies the elements
		/// from the passed collection.
		/// 
		/// Can only be used on a thread having a SynchronizationContext.
		/// Recommended to use on the main thread.
		/// </summary>
		/// <param name="collection">The collection from which the elements are copied.</param>
		/// <param name="sorted">
		/// Is the collection sorted.
		/// T needs to implement IComparable to allow setting this to true.
		/// </param>
		public SuperCollection( List<T> collection, bool sorted )
			: this( (collection ?? new List<T>()).AsEnumerable(), sorted )
		{
		}

		/// <summary>
		/// Constructs a new SuperCollection with the ability to make it sorted and copies the elements
		/// from the passed collection.
		/// 
		/// Can only be used on a thread having a SynchronizationContext.
		/// Recommended to use on the main thread.
		/// </summary>
		/// <param name="collection">The collection from which the elements are copied.</param>
		/// <param name="sorted">
		/// Is the collection sorted.
		/// T needs to implement IComparable to allow setting this to true.
		/// </param>
		public SuperCollection( IEnumerable<T> collection, bool sorted )
			: base( collection )
		{
			m_creatorContext = SynchronizationContext.Current;
			m_creatorContextManagedThreadId = Thread.CurrentThread.ManagedThreadId;

			SetOwnerContextDefault();

			Sorted = sorted;
		}

		#endregion New constructors

		#region New methods

		/// <summary>
		/// Set the default synchronization context of SuperCollection instances
		/// </summary>
		/// <param name="context">
		///		the SynchronizationContext to set as default. If null or left out, the current thread's is used. If the current thread doesn't have one, an exception is thrown.
		///	</param>
		/// <param name="contextManagedThreadId">
		///		the managed thread ID of the thread associated with SynchronizationContext. If null or left out, the current thread's is used.
		///	</param>
		public static void SetDefaultSynchronizationContext( SynchronizationContext context = null, int? contextManagedThreadId = null )
		{
			SuperCollectionStaticNonGeneric.SetDefaultSynchronizationContext( context, contextManagedThreadId );
		}

		/// <summary>
		/// Sets the SynchronizationContext for this instance to be the default one
		/// </summary>
		public void SetOwnerContextDefault()
		{
			if ( SuperCollectionStaticNonGeneric.DefaultOwnerContext == null )
			{
				throw new InvalidOperationException( "The default SynchronizationContext for SuperCollection hasn't been set yet." );
			}

			m_ownerContext = SuperCollectionStaticNonGeneric.DefaultOwnerContext;
			m_ownerContextManagedThreadId = SuperCollectionStaticNonGeneric.DefaultOwnerContextManagedThreadId;
		}

		/// <summary>
		/// Sets the SynchronizationContext for this instance to be the one of the creator.
		/// If the creator doesn't have one, an exception is thrown.
		/// </summary>
		public void SetOwnerContextCreator()
		{
			if ( m_creatorContext == null )
			{
				throw new InvalidOperationException( "The thread creating this SuperCollection doesn't have a SynchronizationContext." );
			}

			m_ownerContext = m_creatorContext;
			m_ownerContextManagedThreadId = m_creatorContextManagedThreadId;
		}

		/// <summary>
		/// Adds the elements of the specified collection to the end of the List.
		/// </summary>
		/// <param name="collection">
		/// The collection whose elements should be added to the end of the List.
		/// The collection itself cannot be null, but it can contain elements that are null,
		/// if type T is a reference type.
		/// </param>
		public void AddRange( IEnumerable<T> collection )
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				bool suppressed = NotificationsSuppressed;

				try
				{
					NotificationsSuppressed = true;

					foreach ( var item in collection )
					{
						Add( item );
					}
				}
				finally
				{
					NotificationsSuppressed = suppressed;
				}
			}

			OnCollectionChanged( new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );
		}

		/// <summary>
		/// Removes the elements of the specified collection from this List.
		/// </summary>
		/// <param name="collection">
		/// The collection whose elements should be removed from this List.
		/// The collection itself cannot be null, but it can contain elements that are null,
		/// if type T is a reference type.
		/// </param>
		public void RemoveRange( IEnumerable<T> collection )
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				bool suppressed = NotificationsSuppressed;

				try
				{
					NotificationsSuppressed = true;

					collection = collection.Intersect( this );

					foreach ( var item in collection )
					{
						Remove( item );
					}
				}
				finally
				{
					NotificationsSuppressed = suppressed;
				}
			}

			OnCollectionChanged( new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );
		}

		/// <summary>
		/// Sorts the collection with the default comparer.
		/// 
		/// Can only be used if T implements IComparable&lt;T&gt;.
		/// </summary>
		/// <param name="descending">True if a descending order needs to be estabilized.</param>
		public void Sort( bool descending = false )
		{
			if ( !(typeof( IComparable<T> ).IsAssignableFrom( typeof( T ) )) )
			{
				throw new InvalidOperationException(
					string.Format(
					"Automatic sorting cannot be called on a SuperCollection of {0}: it does not implement IComparable<{0}>.",
					typeof( T ).Name ) );
			}
			else
			{
				Sort(
					( T first, T second ) =>
					{
						return ((IComparable<T>) first).CompareTo( second );
					},
					descending );
			}
		}

		/// <summary>
		/// Sorts the collection with the passed comparer.
		/// </summary>
		/// <param name="comparison"></param>
		/// <param name="descending">True if a descending order needs to be estabilized.</param>
		public void Sort( Comparison<T> comparison, bool descending = false )
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				bool suppressed = NotificationsSuppressed;

				try
				{
					NotificationsSuppressed = true;

					if ( !descending )
					{
						this.OrderBy( new Func<T, T>( ( T item ) =>
						{
							return item;
						} ),
						new ComparisonComparer<T>( comparison ) );
					}
					else
					{
						this.OrderByDescending( new Func<T, T>( ( T item ) =>
						{
							return item;
						} ),
						new ComparisonComparer<T>( comparison ) );
					}
				}
				finally
				{
					NotificationsSuppressed = suppressed;
				}
			}

			bool sorted = m_sorted;

			try
			{
				m_sorted = false; // prevent resorting on the event
				OnCollectionChanged( new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Reset ) );
			}
			finally
			{
				m_sorted = sorted;
			}
		}

		#region Comparison-Comparer hack

		/// <summary>
		/// Converts a Comparison to an IComparer
		/// </summary>
		/// <typeparam name="Tp"></typeparam>
		/// <remarks>From http://dpatrickcaldwell.blogspot.hu/2009/04/converting-comparison-to-icomparer.html </remarks>
		public class ComparisonComparer<Tp> : IComparer<Tp>
		{
			private readonly Comparison<Tp> _comparison;

			/// <summary>
			/// Constructs a new Comparer from the passed Comparison
			/// </summary>
			/// <param name="comparison"></param>
			public ComparisonComparer( Comparison<Tp> comparison )
			{
				_comparison = comparison;
			}

			/// <summary>
			/// Compares two objects and returns a value indicating whether one is less than, equal to, or greater than the other.
			/// </summary>
			/// <param name="x">The first object to compare.</param>
			/// <param name="y">The second object to compare.</param>
			/// <returns>
			/// Less than zero if x is less than y.
			/// Zero if x equals y.
			/// Greater than zero if x is greater than y.
			/// </returns>
			public int Compare( Tp x, Tp y )
			{
				return _comparison( x, y );
			}
		}

		#endregion Comparison-Comparer hack

		#endregion New methods

		#region Overridden methods

		/// <summary>
		/// Raises the CollectionChanged event with the provided arguments on the thread owning this collection and also
		/// does various management tasks
		/// </summary>
		/// <param name="e"></param>
		protected override void OnCollectionChanged( NotifyCollectionChangedEventArgs e )
		{
			// HACK: work around PCLs missing the Move method in their ObservableCollection for some weird reason
			if ( e.Action.ToString() == "Move" )
			{
				OnItemMoved( new CollectionItemMovedEventArgs( e.OldItems[0], e.OldStartingIndex, e.NewStartingIndex ) );
			}

			if ( !NotificationsSuppressed )
			{
				m_ownerContext.Post( ( o ) => base.OnCollectionChanged( e ), null );
			}

			lock ( ((ICollection) this).SyncRoot )
			{
				if ( !m_suppressItemCollectionChanged )
				{
					OnItemCollectionChanged( e );
				}

				if ( Sorted ) // TODO: hatékonyabban, eleve jó helyre tegye
				{
					bool suppress = m_suppressItemCollectionChanged;

					try
					{
						m_suppressItemCollectionChanged = false;
						Sort();
					}
					finally
					{
						m_suppressItemCollectionChanged = suppress;
					}
				}
			}
		}

		/// <summary>
		/// Raises the PropertyChanged event with the provided arguments on the thread owning this collection
		/// </summary>
		/// <param name="e"></param>
		protected override void OnPropertyChanged( PropertyChangedEventArgs e )
		{
			if ( !NotificationsSuppressed )
			{
				m_ownerContext.Post( ( o ) => base.OnPropertyChanged( e ), null );
			}
		}

		/// <summary>
		/// Removes all items from the collection.
		/// </summary>
		protected override void ClearItems()
		{
			List<T> oldItems;

			lock ( ((ICollection) this).SyncRoot )
			{
				foreach ( var item in this )
				{
					var notifItem = item as INotifyPropertyChanged;

					if ( notifItem != null )
					{
						(notifItem).PropertyChanged -= item_PropertyChanged;
					}
				}

				oldItems = new List<T>( this );

				base.ClearItems();
			}

			OnItemsCleared( new CollectionItemsClearedEventArgs( oldItems ) );
		}

		/// <summary>
		/// Inserts an item into the collection at the specified index.
		/// </summary>
		/// <param name="index">The zero-based index at which item should be inserted.</param>
		/// <param name="item">The object to insert.</param>
		protected override void InsertItem( int index, T item )
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				base.InsertItem( index, item );

				var notifItem = item as INotifyPropertyChanged;

				if ( notifItem != null )
				{
					(notifItem).PropertyChanged -= item_PropertyChanged;
					(notifItem).PropertyChanged += item_PropertyChanged;
				}
			}

			OnItemAdded( new CollectionItemAddedEventArgs( item, index ) );
		}

		/// <summary>
		/// Removes the item at the specified index of the collection.
		/// </summary>
		/// <param name="index">The zero-based index of the element to remove.</param>
		protected override void RemoveItem( int index )
		{
			T item;

			lock ( ((ICollection) this).SyncRoot )
			{
				item = this[index];

				base.RemoveItem( index );

				var notifItem = item as INotifyPropertyChanged;

				if ( notifItem != null )
				{
					(notifItem).PropertyChanged -= item_PropertyChanged;
				}
			}

			OnItemRemoved( new CollectionItemRemovedEventArgs( item, index ) );
		}

		/// <summary>
		/// Replaces the element at the specified index.
		/// </summary>
		/// <param name="index">The zero-based index of the element to replace.</param>
		/// <param name="item">The new value for the element at the specified index.</param>
		protected override void SetItem( int index, T item )
		{
			T oldItem;

			lock ( ((ICollection) this).SyncRoot )
			{
				oldItem = this[index];

				base.SetItem( index, item );

				var notifItem = oldItem as INotifyPropertyChanged;

				if ( notifItem != null )
				{
					(notifItem).PropertyChanged -= item_PropertyChanged;
				}

				notifItem = item as INotifyPropertyChanged;

				if ( notifItem != null )
				{
					(notifItem).PropertyChanged -= item_PropertyChanged;
					(notifItem).PropertyChanged += item_PropertyChanged;
				}
			}

			OnItemReplaced( new CollectionItemReplacedEventArgs( oldItem, item, index ) );
		}

		#endregion Overridden methods

		#region New event methods

		/// <summary>
		/// Raises the ItemChanged event with the provided arguments
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnItemChanged( CollectionItemChangedEventArgs e )
		{
			if ( ItemChanged != null )
			{
				ItemChanged( this, e );
			}
		}

		/// <summary>
		/// Raises the ItemAdded event with the provided arguments
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnItemAdded( CollectionItemAddedEventArgs e )
		{
			if ( ItemAdded != null )
			{
				ItemAdded( this, e );
			}
		}

		/// <summary>
		/// Raises the ItemMoved event with the provided arguments
		/// </summary>
		/// <param name="e"></param>
		private void OnItemMoved( CollectionItemMovedEventArgs e )
		{
			if ( ItemMoved != null )
			{
				ItemMoved( this, e );
			}
		}

		/// <summary>
		/// Raises the ItemRemoved event with the provided arguments
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnItemRemoved( CollectionItemRemovedEventArgs e )
		{
			if ( ItemRemoved != null )
			{
				ItemRemoved( this, e );
			}
		}

		/// <summary>
		/// Raises the ItemsCleared event with the provided arguments
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnItemsCleared( CollectionItemsClearedEventArgs e )
		{
			if ( ItemsCleared != null )
			{
				ItemsCleared( this, e );
			}
		}

		/// <summary>
		/// Raises the ItemReplaced event with the provided arguments
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnItemReplaced( CollectionItemReplacedEventArgs e )
		{
			if ( ItemReplaced != null )
			{
				ItemReplaced( this, e );
			}
		}

		/// <summary>
		/// Raises the ItemCollectionChanged event with the provided arguments
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnItemCollectionChanged( NotifyCollectionChangedEventArgs e )
		{
			if ( !NotificationsSuppressed )
			{
				if ( ItemCollectionChanged != null )
				{
					ItemCollectionChanged( this, e );
				}
			}
		}

		#endregion New event methods

		#region Item change observation

		/// <summary>
		/// Fires a collection reset event when an item fires a property changed one
		/// </summary>
		/// <param name="sender"></param>
		/// <param name="e"></param>
		private void item_PropertyChanged( object sender, PropertyChangedEventArgs e )
		{
			lock ( ((ICollection) this).SyncRoot )
			{
				try
				{
					// TODO: ez itt így veszélyes, ne tüzeljen eventet lock alól, ha lehet
					m_suppressItemCollectionChanged = true;
					OnItemChanged( new CollectionItemChangedEventArgs( sender, IndexOf( (T) sender ), e ) );
					// TODO: legyen itt egyáltalán collection change? Nélküle az ItemCollectionChange-t ki is lehetne hagyni
					OnCollectionChanged( new NotifyCollectionChangedEventArgs( NotifyCollectionChangedAction.Replace, sender, sender, IndexOf( (T) sender ) ) );
				}
				finally
				{
					m_suppressItemCollectionChanged = false;
				}
			}
		}

		#endregion Item change observation
	}

	/// <summary>
	/// Contains the static components of SuperCollection that are shared across all generic versions
	/// </summary>
	public static class SuperCollectionStaticNonGeneric
	{
		/// <summary>
		/// The owner context to set for objects created on a thread without one
		/// </summary>
		private static SynchronizationContext s_defaultOwnerContext;

		/// <summary>
		/// The owner context to set for objects created on a thread without one
		/// </summary>
		public static SynchronizationContext DefaultOwnerContext
		{
			get
			{
				return s_defaultOwnerContext;
			}
		}

		/// <summary>
		/// The managed thread id of the thread associated with s_defaultOwnerContext
		/// </summary>
		private static int s_defaultOwnerContextManagedThreadId;

		/// <summary>
		/// The managed thread id of the thread associated with s_defaultOwnerContext
		/// </summary>
		public static int DefaultOwnerContextManagedThreadId
		{
			get
			{
				return s_defaultOwnerContextManagedThreadId;
			}
		}

		/// <summary>
		/// Set the default synchronization context of SuperCollection instances
		/// </summary>
		/// <param name="context">
		///		the SynchronizationContext to set as default. If null or left out, the current thread's is used. If the current thread doesn't have one, an exception is thrown.
		///	</param>
		/// <param name="contextManagedThreadId">
		///		the managed thread ID of the thread associated with SynchronizationContext. If null or left out, the current thread's is used.
		///	</param>
		public static void SetDefaultSynchronizationContext( SynchronizationContext context = null, int? contextManagedThreadId = null )
		{
			var tempDefaultOwnerContext = context ?? SynchronizationContext.Current;
			var tempDefaultOwnerContextManagedThreadId = contextManagedThreadId ?? Thread.CurrentThread.ManagedThreadId;

			if ( tempDefaultOwnerContext != null )
			{
				s_defaultOwnerContext = tempDefaultOwnerContext;
				s_defaultOwnerContextManagedThreadId = tempDefaultOwnerContextManagedThreadId;
			}
			else
			{
				throw new InvalidOperationException( "SuperCollection.SetDefaultSynchronizationContext with the default arguments must be called from a thread having an associated SynchronizationContext." );
			}
		}
	}
}
